﻿﻿/* **********************************************************************************
 * Copyright (c) 2011 John Hughes
 *
 * j9Script is licenced under the Microsoft Reciprocal License (Ms-RL).
 *
 * Project Website: http://j9Script.codeplex.com/
 * **********************************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Specialized;

namespace J9Script.Internal
{
    internal class ScriptParser
    {
        string _script;
        ScriptPosition _position;
        const char _quotechar = '\"';

        public ScriptParser(string script)
        {
            _script = script;
            _position = ScriptPosition.Empty;
        }

        public ScriptParser(string script, ScriptPosition position)
        {
            _script = script;
            _position = position;
        }

        public string Script
        {
            get { return _script; }
        }

        public ScriptPosition Position
        {
            get { return _position; }
            set { _position = value; }
        }

        public int ScriptLength
        {
            get { return _script.Length; }
        }

        public string Substring(ScriptPosition start, int length)
        {
            return _script.Substring(start.PositionInScript, length);
        }

        public string Substring(ScriptPosition start, ScriptPosition end)
        {
            return _script.Substring(start.PositionInScript, end.PositionInScript - start.PositionInScript);
        }


        /// <summary>
        /// Get's the next token without changing the current state.
        /// </summary>
        /// <returns></returns>
        public Token PeekToken()
        {
            // Remember current state.            
            ScriptPosition pos = _position;

            // Get next token
            Token token = GetToken();

            // Reset state
            _position = pos;

            return token;
        }


        public Token GetToken()
        {
            ScriptPosition start;
            char firstchar;
            TokenTypeEnum tokentype;
            char actualFirstChar = ' '; // default to whitespace

            int scriptLength = _script.Length;

            if (_position.PositionInScript < scriptLength)
                actualFirstChar = _script[_position.PositionInScript];

            // Skip over any initial White Space and Comments
            while ((_position.PositionInScript < scriptLength && char.IsWhiteSpace(_script[_position.PositionInScript]))
                || ((_position.PositionInScript < scriptLength - 1) && _script.Substring(_position.PositionInScript, 2) == "//"))
            {
                // Skip over any initial white space
                while (_position.PositionInScript < scriptLength && char.IsWhiteSpace(_script[_position.PositionInScript]))
                {
                    if (_script[_position.PositionInScript] == '\n')
                        _position.NewLine();
                    else
                        _position.NextCharacter();
                }

                // Skip over comments
                while (_position.PositionInScript < scriptLength - 1 && _script.Substring(_position.PositionInScript, 2) == "//")
                {
                    // find line feed
                    while (_position.PositionInScript < scriptLength && _script[_position.PositionInScript] != '\n')
                        _position.NextCharacter();
                    // don't skip over the posible '\n', so that the white space remover above will count to new line.
                }
            }


            // if we hit the end then return a blank token
            if (_position.PositionInScript >= scriptLength)
                return Token.EndOfScript;

            start = _position;
            firstchar = char.ToUpper(_script[_position.PositionInScript]);


            // if the 1st character is a #, then read all consecutive #'s
            if (char.IsNumber(firstchar) || (firstchar == '.' && char.IsWhiteSpace(actualFirstChar)))
            {
                // Number
                tokentype = TokenTypeEnum.NumberLiteral;
                _position.NextCharacter();
                while (_position.PositionInScript < scriptLength && (char.IsNumber(_script[_position.PositionInScript]) || _script[_position.PositionInScript] == '.'))
                    _position.NextCharacter();
            }
            else if (isOperatorCharacter(firstchar))
            {
                // Operator
                tokentype = TokenTypeEnum.Operator;
                _position.NextCharacter();
                //if (firstchar != '-' && firstchar != '!' && firstchar != ',' && firstchar != '(' && firstchar != ')')
                //if (firstchar != '-' && firstchar != ',' && firstchar != '(' && firstchar != ')' && firstchar != '{' && firstchar != '}' && firstchar != ';' && firstchar != ':' && firstchar != '[' && firstchar != ']')  // removed '!' condition since we could have a "!="
                if (firstchar != ',' && firstchar != '(' && firstchar != ')' && firstchar != '{' && firstchar != '}' && firstchar != ';' && firstchar != ':' && firstchar != '[' && firstchar != ']')  // removed '!' condition since we could have a "!=".  Also remove '=' since we could have "--".
                    while (_position.PositionInScript < scriptLength && isOperatorCharacter(_script[_position.PositionInScript]))
                    {
                        if (_position.PositionInScript > start.PositionInScript + 1 && _script[_position.PositionInScript] == ')') // just in case it's a closing parenthesis after something like ++
                            break;
                        if (firstchar == '!' && _position.PositionInScript == start.PositionInScript + 1 && _script[_position.PositionInScript] != '=') // "!" and "!=" are valid, but nothing else starts with "!".
                            break;
						if (firstchar == '=' && _position.PositionInScript == start.PositionInScript + 1 && _script[_position.PositionInScript] != '=') // "=" and "==" are valid, but nothing else starts with "=".
							break;
                        if (firstchar == '+' && _position.PositionInScript == start.PositionInScript + 1 && _script[_position.PositionInScript] != '+') // "+" and "++" are valid, but nothing else starts with "+".
                            break;
                        if (firstchar == '-' && _position.PositionInScript == start.PositionInScript + 1 && _script[_position.PositionInScript] != '-') // "-" and "--" are valid, but nothing else starts with "-".
                            break;
						_position.NextCharacter();
                    }
            }
            else if (firstchar == ';')
            {
                // End of Statement
                tokentype = TokenTypeEnum.EndOfStatement;
                _position.NextCharacter();
            }
            else if (_position.PositionInScript < scriptLength - 1 && firstchar == '@' && _script[_position.PositionInScript + 1] == _quotechar)
            {
                // Verbatim String
                // Verbatim strings have the format @"...".  They can span lines and '\' is not recognized as an escape symbol.
                // The characters between the quotation marks, including white space such as new line characters, are preserved verbatim.
                // Quotes are escaped using the quote character... @"abc""xyz" = "abc\"xyz"
                tokentype = TokenTypeEnum.VerbatimStringLiteral;
                _position.NextCharacter(); // get past '@'
                _position.NextCharacter(); // get past quote

                while (_position.PositionInScript < scriptLength)
                {
                    // If we find a quote, then check if it is escaped.
                    if (_script[_position.PositionInScript] == _quotechar)
                    {
                        // If the next character is a quote then we are at an escaped quote
                        if (_position.PositionInScript < scriptLength - 1 && _script[_position.PositionInScript + 1] == _quotechar)
                            _position.NextCharacter(); // skip over 1st quote, and then next we will skip over the next quote.
                        else
                            break;
                    }

					if (_script[_position.PositionInScript] == '\n')
						_position.NewLine();
					else
						_position.NextCharacter();
                }
				_position.NextCharacter(); // skip over last quote
            }
            else if (firstchar == _quotechar)
            {
                // String
                tokentype = TokenTypeEnum.RegularStringLiteral;
                _position.NextCharacter();
                while (_position.PositionInScript < scriptLength)
                {
                    // If we find an unescaped quote then we are done.
                    if (_script[_position.PositionInScript] == _quotechar && _script[_position.PositionInScript - 1] != '\\')
                        break;
                    _position.NextCharacter();
                }
                _position.NextCharacter();
            }
            else if (char.IsLetter(firstchar) || firstchar == '_')
            {
                // Identifier: Keyword, variable name, or function name

                // Identifiers must start with a letter or underscore '_' and can only contain letters, numbers, and/or underscores.

                tokentype = TokenTypeEnum.Identifier;
                _position.NextCharacter();
                // removed '.' from the if statement so we can get 'object.object.property' etc.
                //while (_position.PositionInScript < scriptLength && !char.IsWhiteSpace(_script[_position.PositionInScript]) && !isOperatorCharacter(_script[_position.PositionInScript]) && _script[_position.PositionInScript] != '.') 
                //while (_position.PositionInScript < scriptLength && !char.IsWhiteSpace(_script[_position.PositionInScript]) && !isOperatorCharacter(_script[_position.PositionInScript]))
                //3/18/2008 john Added isIdentifierCharacter() funtion.
                while (_position.PositionInScript < scriptLength && isIdentifierCharacter(_script[_position.PositionInScript]))
                {
                    _position.NextCharacter();
                    if (_position.PositionInScript >= scriptLength)
                        break;
                }
            }
            else
                // The first character doesn't match any known valid first character.
                throw new ScriptEvaluatorException("Invalid expression term '" + firstchar + "'.", _position, IdentifyScriptPosition(_position));

            int tokenLength = _position.PositionInScript - start.PositionInScript;
            string tokenstring = _script.Substring(start.PositionInScript, tokenLength).TrimEnd();

            if (tokentype == TokenTypeEnum.EndOfStatement)
                return new Token(null, TokenTypeEnum.EndOfStatement, start, tokenstring);
            else if (tokentype == TokenTypeEnum.RegularStringLiteral)
            {
                // Convert escaped characters if any.
                string escapedString = tokenstring.Substring(1, _position.PositionInScript - start.PositionInScript - 2);
                return new Token(new ScriptString(unescapeString(escapedString)), TokenTypeEnum.RegularStringLiteral, start, tokenstring);
            }
            else if (tokentype == TokenTypeEnum.VerbatimStringLiteral)
            {
                // Convert escaped quotes if any.
                string escapedString = tokenstring.Substring(2, _position.PositionInScript - start.PositionInScript - 3);
                return new Token(new ScriptString(escapedString.Replace("\"\"", "\"")), TokenTypeEnum.RegularStringLiteral, start, tokenstring);
            }
            else if (tokentype == TokenTypeEnum.NumberLiteral)
            {
                // Convert using invariant number format (ie, use "." for decimal point).
                double d = double.Parse(tokenstring, System.Globalization.NumberStyles.Float, System.Globalization.NumberFormatInfo.InvariantInfo);
                return new Token(new ScriptNumber(d), TokenTypeEnum.NumberLiteral, start, tokenstring);
            }
            else if (tokentype == TokenTypeEnum.Identifier)
            {
                // Check if it's a keyword first.
                if (isKeyword(tokenstring))
                {
                    return new Token(new ScriptString(tokenstring), TokenTypeEnum.Keyword, start, tokenstring);
                }
                else
                {
                    // It's not a keyword.

                    switch (tokenstring.ToUpper())
                    {
                        case "TRUE":
                            return new Token(new ScriptBoolean(true), TokenTypeEnum.BooleanLiteral, start, tokenstring);
                        case "FALSE":
                            return new Token(new ScriptBoolean(false), TokenTypeEnum.BooleanLiteral, start, tokenstring);
                        case "NULL":
                            return new Token(null, TokenTypeEnum.NullLiteral, start, tokenstring);
                        default:
                            return new Token(tokenstring, null, TokenTypeEnum.Identifier, start, tokenstring); // it must be a function name or a new variable name
                    }
                }
            }
            else
                return new Token(new ScriptString(tokenstring), tokentype, start, tokenstring);
        }


        /// <summary>
        /// Converts a string with escaped sequences to a normal string.
        /// </summary>
        /// <param name="escapedString"></param>
        /// <returns></returns>
        static string unescapeString(string escapedString)
        {
            // if there is no escape character then don't waste cpu cycles with replace statements.
            if (escapedString.IndexOf('\\') == -1)
                return escapedString;

			// TODO: It would be much quicker to scan character by characer across the string once.

			escapedString = escapedString.Replace(@"\\", "{[!@S-L-A-S-H@!]}"); // We can't just go and replace \\ with \ since if the \ is followed by a t or some other escape character pal then it will get converted in the steps below. So temporariliy replace it with a string that is unlikely to be in the original string and does not contain any slashes.

            escapedString = escapedString.Replace(@"\'", "'");
            escapedString = escapedString.Replace("\\\"", "\"");
            escapedString = escapedString.Replace(@"\\", "\\");
            escapedString = escapedString.Replace(@"\0", "\0");
            escapedString = escapedString.Replace(@"\a", "\a");
            escapedString = escapedString.Replace(@"\b", "\b");

            escapedString = escapedString.Replace(@"\f", "\f");
            escapedString = escapedString.Replace(@"\n", "\n");
            escapedString = escapedString.Replace(@"\r", "\r");
            escapedString = escapedString.Replace(@"\t", "\t");
            escapedString = escapedString.Replace(@"\v", "\v");

			escapedString = escapedString.Replace("{[!@S-L-A-S-H@!]}", "\\");

            // TODO: support other escape sequences:
            //# \uxxxx - Unicode escape sequence for character with hex value xxxx
            //# \xn[n][n][n] - Unicode escape sequence for character with hex value nnnn (variable length version of \uxxxx)
            //# \Uxxxxxxxx - Unicode escape sequence for character with hex value xxxxxxxx (for generating surrogates)

            return escapedString;
        }




        private bool isOperatorCharacter(char c)
        {
            string operators = "*/%+-<>=&|^!,().{}:[]";
            return (operators.IndexOf(c) >= 0);
        }

        private bool isIdentifierCharacter(char c)
        {
            return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_');
        }

        private bool isKeyword(string s)
        {
            // NOTE: this could use a performance enhancement when we get a bunch of keywords.

            StringCollection keywords = new StringCollection();
            string[] arr = new string[] { "NEW", "TRY", "CATCH", "FINALLY", "THROW", "IF", "ELSE", "WHILE", "FOR", "RETURN", "CONTINUE", "BREAK", "GOTO", "END", "FUNCTION" };
            keywords.AddRange(arr);
            return (keywords.Contains(s.ToUpper()));
        }

        public string IdentifyScriptPosition(ScriptPosition pos)
        {
            try
            {
                // Find start of line.
                int i = pos.PositionInScript;
                while (i > 0 && _script[i] != '\r' && _script[i] != '\n')
                    i--;

                // Find end of line.
                int j = pos.PositionInScript;
                while (j < _script.Length - 1 && _script[j] != '\r' && _script[j] != '\n')
                    j++;

                string s = _script.Substring(i, j - i);
                s += "\r\n" + new string(' ', pos.PositionInLine) + "^\r\n";

                return s;
            }
            catch
            {
                return "";
            }
        }

    }
}
